Skip to content

ref(server-utils): Ensure injected modules can reliably be picked up#22386

Open
mydea wants to merge 3 commits into
developfrom
fn/injected-modules-pickup
Open

ref(server-utils): Ensure injected modules can reliably be picked up#22386
mydea wants to merge 3 commits into
developfrom
fn/injected-modules-pickup

Conversation

@mydea

@mydea mydea commented Jul 20, 2026

Copy link
Copy Markdown
Member

Makes orchestrion-injected modules reliably discoverable at runtime, regardless of how (and when) they were injected.

This fixes two scenarios:

Turbopack

Here, only loaders are accepted, not plugins, so there is no build lifecucle hook to emit the aggregate list of instrumented modules at boot the way the webpack plugin does.

Other Bundlers

While here we can/do inject the aggregate list of modules at build time, depending on how the SDK is initialized this may or may not be defined before Sentry.init() is called - when init() is called ahead of the bundled server code with --import, the module list may not be available yet at this point.

The Fix

To fix this, injected modules now announce themselves through a shared set of runtime globals:

  • Build-time injection emits a dependency-free prologue (buildInjectPrologue) per instrumented module (Turbopack) or a boot-time snippet with the full module list (webpack/rollup/vite/esbuild). Both record the module in __SENTRY_ORCHESTRION__.bundler and call the SDK bridge (__SENTRY_ORCHESTRION_ON_INJECT__) so channel subscribers are wired up before the module publishes — this matters when Sentry.init() runs ahead of the bundled server (e.g. via --require).
  • A new invokeOrchestrionInstrumentation helper bridges these announcements to integration setup: it invokes the instrumentation callback immediately for already-injected (build-time) modules, or later when runtime injection happens, and guarantees the callback runs at most once. Bun/Deno register immediately since module tracking is unavailable and channel limits don't apply there.

This is the base of a stack; the follow-up migrates all channel integrations onto this helper.

Comment thread packages/server-utils/src/orchestrion/config/utils.ts
Comment thread packages/nextjs/src/config/turbopack/generateValueInjectionRules.ts Outdated

const INSTRUMENTATION_FN_SYMBOL = Symbol.for('InstrumentationFn');
// oxlint-disable-next-line typescript/no-explicit-any
type InstrumentationFn = ((...args: any[]) => void) & { [INSTRUMENTATION_FN_SYMBOL]?: boolean };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unguarded any in new helper

Low Severity

New InstrumentationFn uses any with only an oxlint disable and no comment explaining why a safer type is impossible. Project review rules require that explanation for new any in SDK source.

Fix in Cursor Fix in Web

Triggered by project rule: PR Review Guidelines for Cursor Bot

Reviewed by Cursor Bugbot for commit dcccddd. Configure here.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is fine

@github-actions

github-actions Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

size-limit report 📦

⚠️ Warning: Base artifact is not the latest one, because the latest workflow run is not done yet. This may lead to incorrect results. Try to re-run all tests to get up to date results.

Path Size % Change Change
@sentry/browser 27.75 kB - -
@sentry/browser - with treeshaking flags 26.18 kB - -
@sentry/browser (incl. Tracing) 46.56 kB - -
@sentry/browser (incl. Tracing + Span Streaming) 48.37 kB - -
@sentry/browser (incl. Tracing, Profiling) 51.36 kB - -
@sentry/browser (incl. Tracing, Replay) 85.84 kB - -
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 75.46 kB - -
@sentry/browser (incl. Tracing, Replay with Canvas) 90.55 kB - -
@sentry/browser (incl. Tracing, Replay, Feedback) 103.21 kB - -
@sentry/browser (incl. Feedback) 44.93 kB - -
@sentry/browser (incl. sendFeedback) 32.55 kB - -
@sentry/browser (incl. FeedbackAsync) 37.69 kB - -
@sentry/browser (incl. Metrics) 28.83 kB - -
@sentry/browser (incl. Logs) 29.05 kB - -
@sentry/browser (incl. Metrics & Logs) 29.76 kB - -
@sentry/react 29.54 kB - -
@sentry/react (incl. Tracing) 48.84 kB - -
@sentry/vue 33.17 kB - -
@sentry/vue (incl. Tracing) 48.52 kB - -
@sentry/svelte 27.78 kB - -
CDN Bundle 30.14 kB - -
CDN Bundle (incl. Tracing) 48.53 kB - -
CDN Bundle (incl. Logs, Metrics) 31.73 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) 49.83 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) 70.99 kB - -
CDN Bundle (incl. Tracing, Replay) 86.05 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) 87.35 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) 91.85 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) 93.12 kB - -
CDN Bundle - uncompressed 89.88 kB - -
CDN Bundle (incl. Tracing) - uncompressed 146.73 kB - -
CDN Bundle (incl. Logs, Metrics) - uncompressed 94.59 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed 150.71 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed 219.35 kB - -
CDN Bundle (incl. Tracing, Replay) - uncompressed 265.96 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed 269.93 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 279.67 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed 283.62 kB - -
@sentry/nextjs (client) 51.36 kB - -
@sentry/sveltekit (client) 46.99 kB - -
@sentry/core/server 80.26 kB - -
@sentry/core/browser 66.69 kB - -
@sentry/node-core 63.21 kB - -
@sentry/node 125.73 kB - -
@sentry/node (incl. diagnostics channel injection) 149.54 kB +0.02% +22 B 🔺
@sentry/node/import (ESM hook with diagnostics-channel injection) 70.03 kB +0.01% +2 B 🔺
@sentry/node/light 51.32 kB +0.01% +2 B 🔺
@sentry/node - without tracing 74.92 kB - -
@sentry/aws-serverless 84.15 kB - -
@sentry/cloudflare (withSentry) - minified 197.51 kB - -
@sentry/cloudflare (withSentry) 485.87 kB - -

View base workflow run

@mydea
mydea marked this pull request as ready for review July 21, 2026 07:43
@mydea
mydea requested review from a team as code owners July 21, 2026 07:43
@mydea
mydea requested review from JPeer264, chargome, isaacs and nicohrubec and removed request for a team July 21, 2026 07:43
mydea and others added 2 commits July 21, 2026 10:34
The Turbopack value-injection seeded `__SENTRY_ORCHESTRION__` by assigning a
fresh `{ bundler: [] }` object, replacing any existing marker. Unlike the Bun
banner and `buildInjectPrologue`, this was not merge-safe, so a hybrid
runtime+bundler setup could wipe already-recorded runtime module names before
integrations read them.

The value-injection loader can only emit plain assignments, so it now accepts an
optional raw `prefixCode` and the marker is injected as a merge-safe snippet that
only creates the global/`bundler` array when absent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@mydea
mydea force-pushed the fn/injected-modules-pickup branch from 1ba8963 to 3e8c1ea Compare July 21, 2026 08:34
Comment thread packages/nextjs/src/config/turbopack/constructTurbopackConfig.ts Outdated
';(function(){try{' +
'var g=(globalThis.__SENTRY_ORCHESTRION__=globalThis.__SENTRY_ORCHESTRION__||{});' +
`var m=${names};` +
'g.bundler=m;' +

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: The buildInjectBootSnippet function uses a destructive assignment (g.bundler=m;) which could overwrite pre-existing module records, unlike other parts of the code that use a merge-safe pattern.
Severity: LOW

Suggested Fix

To ensure consistency and prevent potential data loss, modify the assignment in buildInjectBootSnippet to be merge-safe. Change 'g.bundler=m;' to a pattern that appends to the existing array, such as 'g.bundler=[...((g.bundler)||[]),...m];'. This aligns with the defensive coding practices used elsewhere in the pull request.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: packages/server-utils/src/orchestrion/bundler/inject.ts#L50

Potential issue: The `buildInjectBootSnippet` function uses a destructive assignment
(`'g.bundler=m;'`) to set the bundler modules. This will overwrite any pre-existing
values on the global `__SENTRY_ORCHESTRION__.bundler` object. While the current
architecture may separate bundler paths (e.g., Turbopack vs. Webpack), preventing this
issue in typical setups, it's an inconsistent and non-defensive pattern. Other parts of
the codebase, such as `buildInjectPrologue`, were explicitly updated to use a merge-safe
approach. This oversight could lead to silent loss of module registration data in more
complex or future hybrid build configurations.

Did we get this right? 👍 / 👎 to inform future reviews.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 6098cab. Configure here.

export function getOrchestrionInjectedModules(): string[] {
const { runtime, bundler } = GLOBAL_OBJ.__SENTRY_ORCHESTRION__ ?? {};
return [...(runtime ?? []), ...(bundler ?? [])];
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Legacy bundler marker crashes module list

Medium Severity

getOrchestrionInjectedModules spreads runtime and bundler after a nullish check only. Legacy Bun banners (and other callers) still use bundler: true, and true ?? [] stays true, so [...true] throws at runtime. That will break invokeOrchestrionInstrumentation for apps that upgrade the SDK without rebuilding.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 6098cab. Configure here.

@chargome
chargome requested a review from timfish July 21, 2026 13:02

/**
* Run the provided instrumentation callback when one of the provided module names is orchestrion-injected.
* If it is already injected, it will invoce the callback immediately (e.g. when build-time injection is used).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* If it is already injected, it will invoce the callback immediately (e.g. when build-time injection is used).
* If it is already injected, it will invoke the callback immediately (e.g. when build-time injection is used).

Comment on lines +2 to +3
// announce itself. Kept dependency-free so the Turbopack loader can import it in the build process
// without pulling in the SDK runtime. The matching runtime side lives in `../instrumentation.ts`

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we're still pulling in sdk code transtively in the loader with this pr (via config/utils) - which should not be an issue I think

@timfish

timfish commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Once this is merged released, you can trivially override the tracingChannelImport transform in orchestrion:

Overriding this transform allows you to inject code into the top of every file that orchestrion successfully injects instrumentations into. The above linked PR allows you to call the original/overridden tracingChannelImport transform which retain the diagnostics channel import.

Check out this example in the bundler plugin docs:

That example was pre-being able to overridetracingChannelImport which makes it even easier as you don't need phantom instrumentations to inject.

@JPeer264 will be using this to inject bundle-time integration registration for Cloudflare (maybe AWS will use this too). Because Cloudflare doesn't have runtime injection, we can use this to only include integrations in the resulting bundle if the user actually depends on the library.

For example this would register an integration and result in it being included in the bundle:

const inject = `import { register${moduleName}Integration } from '@sentry/server-utils';
register${moduleName}Integration();`

Do we need two implementations or can we have the same code solve both of your issues?

// The package name a module path belongs to, e.g. `.../node_modules/ioredis/built/Redis.js` → `ioredis`
// and `.../node_modules/@scope/pkg/x.js` → `@scope/pkg`. Uses the last `node_modules` segment so nested
// dependencies resolve to their own name, and normalizes Windows separators.
function moduleNameFromPath(id: string): string | undefined {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We already depend on module-details-from-path that does this and has been performance tuned.

@@ -0,0 +1,60 @@
// EXPERIMENTAL — orchestrion code-transform loader for Turbopack.
//
// Turbopack accepts webpack loaders but not plugins, so — unlike the webpack path, where the plugin

@timfish timfish Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The bundler plugins already have a webpack loader:
https://github.com/apm-js-collab/code-transformer-bundler-plugins/blob/2e0fcaf6f08baa1a46db90ba87a505a11d51c53a/src/webpack-loader.ts

This has had bug fixes to get the regex matching working because the config args get serialised:
apm-js-collab/code-transformer-bundler-plugins#34

@mydea

mydea commented Jul 22, 2026

Copy link
Copy Markdown
Member Author

@timfish

Do we need two implementations or can we have the same code solve both of your issues?

no! If we can solve this differently/better, all for it. My question now is, can/should we merge this and then adjust it in a follow up to be more streamlined, or would you rather do this directly here?

@timfish

timfish commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

can/should we merge this and then adjust it in a follow up

Happy to streamline the injection later!

This PR should probably use the existing loader though.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants